// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE

;(function(mod) {
    if (typeof exports == 'object' && typeof module == 'object')
        // CommonJS
        mod(require('codemirror'), require('./sql-mode'))
    else if (typeof define == 'function' && define.amd)
        // AMD
        define(['codemirror', './sql-mode'], mod)
    // Plain browser env
    else mod(CodeMirror)
})(function(CodeMirror) {
    'use strict'

    var tables
    var defaultTable
    var keywords
    var identifierQuote
    var CONS = {
        QUERY_DIV: ';',
        ALIAS_KEYWORD: 'AS',
    }
    var Pos = CodeMirror.Pos,
        cmpPos = CodeMirror.cmpPos

    function isArray(val) {
        return Object.prototype.toString.call(val) == '[object Array]'
    }

    function getKeywords(editor) {
        var mode = editor.doc.modeOption
        if (mode === 'sql') mode = 'text/x-sql'
        return CodeMirror.resolveMode(mode).keywords
    }

    function getIdentifierQuote(editor) {
        var mode = editor.doc.modeOption
        if (mode === 'sql') mode = 'text/x-sql'
        return CodeMirror.resolveMode(mode).identifierQuote || '`'
    }

    function getText(item) {
        return typeof item == 'string' ? item : item.text
    }

    function wrapTable(name, value) {
        if (isArray(value)) value = { columns: value }
        if (!value.text) value.text = name
        return value
    }

    function parseTables(input) {
        var result = {}
        if (isArray(input)) {
            for (var i = input.length - 1; i >= 0; i--) {
                var item = input[i]
                result[getText(item).toUpperCase()] = wrapTable(getText(item), item)
            }
        } else if (input) {
            for (var name in input) result[name.toUpperCase()] = wrapTable(name, input[name])
        }
        return result
    }

    function getTable(name) {
        return tables[name.toUpperCase()]
    }

    function shallowClone(object) {
        var result = {}
        for (var key in object) if (object.hasOwnProperty(key)) result[key] = object[key]
        return result
    }

    function match(string, word) {
        var len = string.length
        var sub = getText(word).substr(0, len)

        // console.log(string, sub)
        return string.toUpperCase() === sub.toUpperCase()
    }

    function addMatches(result, search, wordlist, formatter) {
        if (isArray(wordlist)) {
            for (var i = 0; i < wordlist.length; i++)
                if (match(search, wordlist[i])) result.push(formatter(wordlist[i]))
        } else {
            for (var word in wordlist)
                if (wordlist.hasOwnProperty(word)) {
                    var val = wordlist[word]
                    if (!val || val === true) val = word
                    else
                        val = val.displayText
                            ? { text: val.text, displayText: val.displayText }
                            : val.text
                    if (match(search, val)) result.push(formatter(val))
                }
        }
    }

    function cleanName(name) {
        // Get rid name from identifierQuote and preceding dot(.)
        if (name.charAt(0) == '.') {
            name = name.substr(1)
        }
        // replace doublicated identifierQuotes with single identifierQuotes
        // and remove single identifierQuotes
        var nameParts = name.split(identifierQuote + identifierQuote)
        for (var i = 0; i < nameParts.length; i++)
            nameParts[i] = nameParts[i].replace(new RegExp(identifierQuote, 'g'), '')
        return nameParts.join(identifierQuote)
    }

    function insertIdentifierQuotes(name) {
        var nameParts = getText(name).split('.')
        for (var i = 0; i < nameParts.length; i++)
            nameParts[i] =
                identifierQuote +
                // doublicate identifierQuotes
                nameParts[i].replace(
                    new RegExp(identifierQuote, 'g'),
                    identifierQuote + identifierQuote
                ) +
                identifierQuote
        var escaped = nameParts.join('.')
        if (typeof name == 'string') return escaped
        name = shallowClone(name)
        name.text = escaped
        return name
    }

    function nameCompletion(cur, token, result, editor) {
        // Try to complete table, column names and return start position of completion
        var useIdentifierQuotes = false
        var nameParts = []
        var start = token.start
        var cont = true
        while (cont) {
            cont = token.string.charAt(0) == '.'
            useIdentifierQuotes = useIdentifierQuotes || token.string.charAt(0) == identifierQuote

            start = token.start
            nameParts.unshift(cleanName(token.string))

            token = editor.getTokenAt(Pos(cur.line, token.start))
            if (token.string == '.') {
                cont = true
                token = editor.getTokenAt(Pos(cur.line, token.start))
            }
        }

        // Try to complete table names
        var string = nameParts.join('.')
        addMatches(result, string, tables, function(w) {
            return useIdentifierQuotes ? insertIdentifierQuotes(w) : w
        })

        // Try to complete columns from defaultTable
        addMatches(result, string, defaultTable, function(w) {
            return useIdentifierQuotes ? insertIdentifierQuotes(w) : w
        })

        // Try to complete columns
        string = nameParts.pop()
        var table = nameParts.join('.')

        var alias = false
        var aliasTable = table
        // Check if table is available. If not, find table by Alias
        if (!getTable(table)) {
            var oldTable = table
            table = findTableByAlias(table, editor)
            if (table !== oldTable) alias = true
        }

        var columns = getTable(table)
        if (columns && columns.columns) columns = columns.columns

        if (columns) {
            addMatches(result, string, columns, function(w) {
                var tableInsert = table
                if (alias == true) tableInsert = aliasTable
                if (typeof w == 'string') {
                    w = tableInsert + '.' + w
                } else {
                    w = shallowClone(w)
                    w.text = tableInsert + '.' + w.text
                }
                return useIdentifierQuotes ? insertIdentifierQuotes(w) : w
            })
        }

        return start
    }

    function eachWord(lineText, f) {
        var words = lineText.split(/\s+/)
        for (var i = 0; i < words.length; i++) if (words[i]) f(words[i].replace(/[,;]/g, ''))
    }

    function findTableByAlias(alias, editor) {
        var doc = editor.doc
        var fullQuery = doc.getValue()
        var aliasUpperCase = alias.toUpperCase()
        var previousWord = ''
        var table = ''
        var separator = []
        var validRange = {
            start: Pos(0, 0),
            end: Pos(editor.lastLine(), editor.getLineHandle(editor.lastLine()).length),
        }

        //add separator
        var indexOfSeparator = fullQuery.indexOf(CONS.QUERY_DIV)
        while (indexOfSeparator != -1) {
            separator.push(doc.posFromIndex(indexOfSeparator))
            indexOfSeparator = fullQuery.indexOf(CONS.QUERY_DIV, indexOfSeparator + 1)
        }
        separator.unshift(Pos(0, 0))
        separator.push(Pos(editor.lastLine(), editor.getLineHandle(editor.lastLine()).text.length))

        //find valid range
        var prevItem = null
        var current = editor.getCursor()
        for (var i = 0; i < separator.length; i++) {
            if (
                (prevItem == null || cmpPos(current, prevItem) > 0) &&
                cmpPos(current, separator[i]) <= 0
            ) {
                validRange = { start: prevItem, end: separator[i] }
                break
            }
            prevItem = separator[i]
        }

        var query = doc.getRange(validRange.start || { line: 0, ch: 0 }, validRange.end, false)

        for (var i = 0; i < query.length; i++) {
            var lineText = query[i]
            eachWord(lineText, function(word) {
                var wordUpperCase = word.toUpperCase()
                if (wordUpperCase === aliasUpperCase && getTable(previousWord)) table = previousWord
                if (wordUpperCase !== CONS.ALIAS_KEYWORD) previousWord = word
            })
            if (table) break
        }
        return table
    }

    CodeMirror.registerHelper('hint', 'sql', function(editor, options) {
        tables = parseTables(options && options.tables)
        var defaultTableName = options && options.defaultTable
        var disableKeywords = options && options.disableKeywords
        defaultTable = defaultTableName && getTable(defaultTableName)
        keywords = getKeywords(editor)
        identifierQuote = getIdentifierQuote(editor)

        if (defaultTableName && !defaultTable)
            defaultTable = findTableByAlias(defaultTableName, editor)

        defaultTable = defaultTable || []

        if (defaultTable.columns) defaultTable = defaultTable.columns

        var cur = editor.getCursor()
        var result = []
        var token = editor.getTokenAt(cur),
            start,
            end,
            search
        if (token.end > cur.ch) {
            token.end = cur.ch
            token.string = token.string.slice(0, cur.ch - token.start)
        }

        // let reference = options.referenceFn || (s => '#' + s);
        // let refRe = new RegExp(_.escapeRegExp(reference('SPLITTER')).replace('SPLITTER', '\\b(\\w*)\\b'))
        if (
            token.string.match(/^[.`"\w@]\w*$/) ||
            (options.refRe && token.string.match(options.refRe))
        ) {
            search = token.string
            start = token.start
            end = token.end
        } else {
            start = end = cur.ch
            search = ''
        }
        if (search.charAt(0) == '.' || search.charAt(0) == identifierQuote) {
            start = nameCompletion(cur, token, result, editor)
        } else {
            // console.log(tables)

            // addMatches(result, search, tables, function(w) {return w;});
            addMatches(result, search, tables, function(w) {
                return { displayText: w, text: identifierQuote + w + identifierQuote }
            })

            for (let table_name in tables) {
                // addMatches(result, search, tables[table_name].columns, function(w) {
                //   return w;
                // });
                addMatches(result, search, tables[table_name].columns, function(w) {
                    // return { displayText: w, text: identifierQuote + table_name + identifierQuote + '.' + identifierQuote + w + identifierQuote };
                    return { displayText: w, text: identifierQuote + w + identifierQuote }
                })
            }
            addMatches(result, search, defaultTable, function(w) {
                return w
            })

            if (!disableKeywords) {
                let compoundKeywordBlacklist = [
                    'GROUP',
                    'ORDER',
                    'INDEXED',
                    'BY',
                    'ALTER',
                    'DETACH',
                    'ATTACH',
                    'DATABASE',
                    'BEGIN',
                    'COMMIT',
                    'ROLLBACK',
                    'END',
                ]
                let compoundKeywordWhitelist = [
                    'GROUP BY',
                    'ORDER BY',
                    'INDEXED BY',
                    'ALTER TABLE',
                    'ATTACH DATABASE',
                    'DETACH DATABASE',
                    'BEGIN TRANSACTION',
                    'COMMIT TRANSACTION',
                    'ROLLBACK TRANSACTION',
                    'END TRANSACTION',
                ]
                addMatches(
                    result,
                    search,
                    compoundKeywordWhitelist.concat(
                        Object.keys(keywords).filter(
                            (k) =>
                                k.length > 3 && !compoundKeywordBlacklist.includes(k.toUpperCase())
                        )
                    ),
                    function(w) {
                        return w.toUpperCase()
                    }
                )
            }
        }

        return { list: result, from: Pos(cur.line, start), to: Pos(cur.line, end) }
    })
})
